Medium 好讀版點此。
講到「分支」的時候,大家腦中浮現的是什麼畫面?
是不是類似這樣:
但在 git 內部,分支並不是這樣運作的,並沒有「一個一個 commit 連線成分支」這種結構,所謂 main、feature 這些分支名稱其實都是在不同 commit 之間移動的「參考(references,簡稱 refs)」,其相關資訊都儲存在 .git/refs/ 資料夾中。
現在讓我們回顧《Day 6-上層瓷器指令複習(本地端分支管理)》的部分操作,觀察下這些指令後,.git/ 資料夾發生的變化。
觀察的對象包含:
.git/
├── hooks/
├── info/
├── objects/
├── refs/
│ ├── heads/ # 觀察這個資料夾
│ └── tags/
│
├── config
├── description
└── HEAD # 觀察這個檔案
在經過 git init 指令後,預設的分支只有 master 或 main(我們的範例用 main),而當沒有任何 commit 時,資料夾結構長這樣:
.git/
├── ...
├── refs/
│ ├── heads/ # 空
│ └── tags/
│
├── ...
└── HEAD # ref: refs/heads/main
而當形成第一個 commit 之後,變成:
.git/
├── ...
├── refs/
│ ├── heads/ # 跑出一個main檔案
│ │ └── main # 內容為87a047450366235cfc5afa5d9f11463b6b17c067
│ └── tags/
│
├── ...
└── HEAD # 一樣為ref: refs/heads/main
.refs/heads/ 資料夾多了一個 main 檔案,內容為 87a0474... ,跟用 git log 看到剛剛 commit 的雜湊碼一樣:
至於那個 HEAD 是什麼?資料夾中的 HEAD 檔案寫著 ref: refs/heads/main,透過 git log 則顯示 HEAD -> main,兩者看起來有一些關聯。
HEAD 本身其實是「指向另外一個參考的參考(reference to another reference)」,表示「當下在哪個分支上」,所以 HEAD 裡面寫 ref: refs/heads/main 會造就 git log 跑出 HEAD -> main,表示「目前在 main 分支上。
結合剛剛的 main 參考與 HEAD 參考,整體結構可透過下圖理解:

由於 HEAD 本身就是一個參考,而它指的對象 main 也是一個參考,因此說 HEAD 是「指向另外一個參考的參考」。
我們再透過 git branch 做出新分支 featureA 與 featureB,則 ./git 資料夾變成這樣:
.git/
├── ...
├── refs/
│ ├── heads/ # 新建什麼分支,這裡就多了以該分支為名的檔案
│ │ └── main # 內容為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureA # 內容為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureB # 內容為87a047450366235cfc5afa5d9f11463b6b17c067
│ └── tags/
│
├── ...
└── HEAD # 一樣為ref: refs/heads/main
看來 main、featureA 與 featureB 都指向 87a0474... 這個 commit,而 HEAD 檔案為 ref: refs/heads/main,表示現在在 main 這個分支上,圖示如下:
經過 git switch 改變分支,其實就是在改變 HEAD 指的對象,例如透過以下指令:
git switch featureA
則 .git/ 資料夾結構變成:
.git/
├── ...
├── refs/
│ ├── heads/ # 沒新建分支,因此裡面的檔案不變
│ │ └── main # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureA # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureB # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ └── tags/
│
├── ...
└── HEAD # 變成ref: refs/heads/featureA
圖示如下:
如果再輸入以下指令切換到 featureB 分支:
git switch featureA
則 .git/ 資料夾變成:
.git/
├── ...
├── refs/
│ ├── heads/ # 這裡一樣沒有變化
│ │ └── main # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureA # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureB # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ └── tags/
│
├── ...
└── HEAD # 變成ref: refs/heads/featureB
圖示如下:
現在我們在 featureA 分支上建立一個新 commit,再下一次 git log 指令會觀察到什麼?
接著觀察資料夾結構:
.git/
├── ...
├── refs/
│ ├── heads/
│ │ └── main # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureA # 變成67250cdc18cc049fb57d83abcc2eb691cc5d860f
│ │ └── featureB # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ └── tags/
│
├── ...
└── HEAD # 在main分支上時為ref: refs/heads/main
# 在featureA分支上時為ref: refs/heads/featureA
# 在featureB分支上時為ref: refs/heads/featureB
在 featureA 多了一個 commit 後,refs/heads/featureA 指向那個新的 commit 67250cd...,refs/heads/main 跟 refs/heads/featureB 都不變。
而透過 git switch 切換分支,會改變 HEAD 檔案的內容,導致 git log 出來的結果跟著改變,但不變的原則是:
HEAD:始終指向當下所在的分支。main、featureA、featureB:始終指向該分支上最新的 commit,圖示如下:
那如果在不同分支上都新建一個 commit,會發生什麼事?
因為是在 featureA 分支上新增一個 commit,featureA 標籤會移到該分支最新的 commit 8a4de7e... 上,而 HEAD 指向當前分支,因此會隨 featureA 這個參考移動:
.git/
├── ...
├── refs/
│ ├── heads/
│ │ └── main # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ │ └── featureA # 變成8a4de7e7cb983354be514221ba78c5fcf8534152
│ │ └── featureB # 依然為87a047450366235cfc5afa5d9f11463b6b17c067
│ └── tags/
│
├── ...
└── HEAD # ref: refs/heads/featureA

切換回 main 分支,並在 main 分支上新增一個 commit 後,以 git log 指令檢查會發現,main 這項參考移動到該分支的最新 commit 530c49a... 上,而因為 HEAD 會指向當前分支 main,所以也會跟著移動:
.git/
├── ...
├── refs/
│ ├── heads/
│ │ └── main # 530c49adabe0faacc6a6f429358110b123dd5dd4
│ │ └── featureA # 8a4de7e7cb983354be514221ba78c5fcf8534152
│ │ └── featureB # 87a047450366235cfc5afa5d9f11463b6b17c067
│ └── tags/
│
├── ...
└── HEAD # ref: refs/heads/main

切換到 featureB 分支新建一個 commit,跟剛剛在 main 分支上新建 commit 效果相同:
.git/
├── ...
├── refs/
│ ├── heads/
│ │ └── main # 530c49adabe0faacc6a6f429358110b123dd5dd4
│ │ └── featureA # 8a4de7e7cb983354be514221ba78c5fcf8534152
│ │ └── featureB # 271c26d3a74da11d09d9a28db56318f46592a140
│ └── tags/
│
├── ...
└── HEAD # ref: refs/heads/featureB
在這篇文章中,我們探究了 .git/ 資料夾中的 refs/heads/ 資料夾與 HEAD 檔案,整理出以下兩大重點:
refs/heads:存放分支的名字,每個分支內容為該分支上最新 commit 的雜湊碼,代表指向對應分支上的最新 commit。HEAD:寫著當下所在的分支。簡單來說,兩者都是「參考」,或者可以想成指標(pointer),分支的名字指向對應分支的最新 commit、HEAD 則指向當下所在的分支名稱。